<?php

/**
 * Louder Cache
 *
 * LICENSE
 *
 * This source file is subject to the New BSD License that is bundled
 * with this package in the file docs/LICENSE.txt.
 *
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to info@loudertechnology.com so we can send you a copy immediately.
 *
 * @category Louder
 * @package ClusterServer
 * @copyright Copyright (c) 2005-2009 Andres Felipe Gutierrez (gutierrezandresfelipe at gmail.com)
 * @license New BSD License
 */

/**
 * Servidor que administrar las peticiones al cluster
 *
 * @category Louder
 * @package ClusterServer
 * @copyright Copyright (c) 2005-2009 Andres Felipe Gutierrez (gutierrezandresfelipe at gmail.com)
 * @license New BSD License
 */
class ClusterServer {

	/**
	 * Version del Clustering Server
	 *
	 */
	const VERSION = "0.7.0a";

	/**
	 * Nombre de la Instancia
	 *
	 * @var string
	 */
	private static $_instanceName;

	/**
	 * Archivo de Configuracion
	 *
	 * @var string
	 */
	private static $_configFile = "partitions/partition.xml";

	/**
	 * Administra las peticiones hechas al cluster
	 *
	 * @access public
	 * @static
	 */
	public static function handleRequests(){
		$requestHandled = false;
		$retries = 1;
		while($requestHandled==false){
			try {
				if($retries==1){
					if(file_exists(self::$_configFile)==true){
						ClusterConfig::createConfig(self::$_configFile);
						$config = ClusterConfig::read(self::$_configFile);
						foreach($config as $instanceName => $nodes){
							self::$_instanceName = $instanceName;
							foreach($nodes as $nodeId => $nodeDescriptor){
								ClusterNodeManager::addNode(new ClusterNode($nodeDescriptor));
							}
						}
					} else {
						throw new ClusterServerException("No se pudo leer la configuración de la partición");
					}
					$requestedURL = self::_prepareNodeRequestURI();
					$balancer = LoadBalancer::getDefaultBalancer();
				}
				$node = $balancer->getSelectedNode(ClusterNodeManager::getManagedNodes());
				$nodeAddress = $node->getAddress();
				$scheme = self::_getProtocolScheme($node);
				$request = ClusterRequest::getInstance();
				$communicator = new NodeCommunicator($scheme, $nodeAddress, $requestedURL);
				$communicator->setSessionClusterId('X-Cluster-SessID', $request->getSessionId());
				$communicator->prepareQueryData();
				$communicator->prepareHeaders();
				$communicator->sendRequest();
				self::_sendHeadersToClient($communicator->getResponseHeaders());
				if($communicator->isResponseStatusOK()==true){
					if($communicator->isResponseContentType(array('text/html', 'text/css'))){
						$responseBody = self::_applyTransparentClusterState($communicator->getResponseBody());
						self::_outputClientResponse($responseBody);
					} else {
						self::_outputClientResponse($communicator->getResponseBody());
					}
				} else {
					self::_showHTTPDocument(404);
				}
				$requestHandled = true;
			}
			catch(NodeCommunicatorException $e){
				ClusterNodeManager::addFailuredNode($node);
			}
			catch(Exception $e){
				self::_notifyException($e);
				ClusterNodeManager::addFailuredNode($node);
			}
			if($retries>=30){
				throw ClusterServerException("Todos los nodos están caidos");
				exit;
			}
			$retries++;
		}
	}

	/**
	 * Graba en un log la excepción ocurrida
	 *
	 * @access private
	 * @param Exception $e
	 * @static
	 */
	private static function _notifyException($e){
		self::_setTimezone();
		$dir = date("Ymd");
		if(is_dir("logs/$dir")==false){
			mkdir("logs/$dir");
		}
		$logger = new Logger('File', 'logs/'.$dir.'/exceptions.log');
		$logger->log($e->getMessage(), Logger::ERROR);
		$logger->close();
	}

	/**
	 * Establece el timezone
	 *
	 * @access private
	 * @static
	 */
	private static function _setTimezone(){
		$config = Config::read("config/config.ini");
		if(isset($config->core->timezone)){
			date_default_timezone_set($config->core->timezone);
		} else {
			date_default_timezone_set("America/Bogota");
		}
	}

	/**
	 * Obtiene el Scheme requerido
	 *
	 * @access private
	 * @param string $protocol
	 * @static
	 */
	private static function _getProtocolScheme(ClusterNode $node){
		if($node->getProtocol()=='tcp'){
			return 'http';
		}
		if($node->getProtocol()=='ssl'){
			return 'https';
		}
		throw new ClusterServerException("Protocolo invalido en el servidor '".$node->getName()."'");
	}

	/**
	 * Obtiene la URL que se esta solicitando en el Nodo escogido
	 *
	 * @access private
	 * @static
	 */
	private static function _prepareNodeRequestURI(){
		if(isset($_GET['_url'])){
			$_url = $_GET['_url'];
			unset($_GET['_url']);
		} else {
			$_url = self::$_instanceName."/";
		}
		return $_url;
	}

	/**
	 * Envia los headers recibidos de la peticion al cliente
	 *
	 * @param array $headers
	 */
	private static function _sendHeadersToClient($headers){
		foreach($headers as $headerName => $headerValue){
			header("$headerName: $headerValue", true);
		}
		#print_r($headers);
	}

	/**
	 * Reemplaza las URLS por URLs del cluster
	 *
	 * @access public
	 * @param string $responseBody
	 * @return string
	 * @static
	 */
	private static function _applyTransparentClusterState($responseBody){
		return str_replace('/'.self::$_instanceName.'/', '/cluster/'.self::$_instanceName.'/', $responseBody);
	}

	/**
	 * Envia la respuesta al Cliente
	 *
	 * @access public
	 * @param string $responseBody
	 * @static
	 */
	private static function _outputClientResponse($responseBody){
		print $responseBody;
	}

	/**
	 * Envia un documento con informacion del Error
	 *
	 * @param int $errorCode
	 */
	private static function _showHTTPDocument($errorCode){
		if(file_exists("pages/$errorCode.html")){
			require "pages/$errorCode.html";
		} else {
			print $errorCode;
		}
	}

}